package ar.com.e2solver.solver;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import ar.com.e2solver.conf.PuzzleTheme;
import ar.com.e2solver.exception.BusinessException;
import ar.com.e2solver.model.Puzzle;
import ar.com.e2solver.solver.operator.crossover.CrossoverOperator;
import ar.com.e2solver.solver.operator.crossover.CrossoverOperatorFactory;
import ar.com.e2solver.solver.operator.localsearch.mt.WeightedSwapHillClimbing;
import ar.com.e2solver.solver.operator.mutation.MutationOperator;
import ar.com.e2solver.solver.operator.mutation.MutationOperatorFactory;
import ar.com.e2solver.solver.operator.mutation.impl.RegionRotateMutationOperator;

public class GeneticAlgorithmSolver {
	
	private static final int MUTATION_P = 25;
	private static final double ELITISM_P = 0.2;

	private PuzzleTheme thePuzzleTheme;
	private int populationSize;
	private int iterationNumber;
	private Population population;
	private boolean showLog;
	
	public GeneticAlgorithmSolver(PuzzleTheme puzzleTheme, int populationSize, int iterationsNumber, boolean showLog){
		this.thePuzzleTheme = puzzleTheme;
		this.populationSize = populationSize;
		this.iterationNumber = iterationsNumber;
		this.population = new Population();
		this.showLog = showLog;
	}
	
	public GAResult solve() throws BusinessException{
		long initialMilisecs = System.currentTimeMillis();
		int iteration = 0;
		population.addAll(generatePopulation(getPuzzleTheme(), this.populationSize));
		
		Individual best = this.population.getBestIndividual();
		Individual populationBest = best;
		Individual bestIt = best;
		int maxPossibleFitness = best.getMaxPossibleFitness();
		boolean solved = false;
		
		List<Individual> selectedIndividuals = null;
		List<Individual> newIndividuals = null;
		int evolutionSize = (int) Math.floor(this.populationSize * (1 - ELITISM_P));
		
		for(iteration = 0; iteration < getIterationNumber() && !solved; iteration++){
						
			newIndividuals = evolve(MUTATION_P, evolutionSize);
			mergeInANewGeneration(newIndividuals);
			
			if(iteration != 0 && iteration % 150 == 0){
				selectedIndividuals = RouletteWheelSelector.select(this.population, (int)Math.floor(this.population.getSize() * 0.5));
				newIndividuals = WeightedSwapHillClimbing.getInstance().climb(selectedIndividuals);
				mergeClimbed(selectedIndividuals, newIndividuals);
			}
			
			bestIt = this.population.getBestIndividual();
			if(bestIt.getFitnessValue() > best.getFitnessValue()){
				best = new Individual(bestIt.getPuzzle().clone());
				solved = best.getFitnessValue() == maxPossibleFitness;
			}
			
			if(iteration % 1000 == 0){
				if(isShowLog()){
					System.out.println(buildLineToShow(iteration, populationBest, best, initialMilisecs));
				}
				populationBest = best;
			}
		}
		
		long finalMilisecs = System.currentTimeMillis();
		return new GAResult(population, finalMilisecs - initialMilisecs, iteration, best);
	}


	private List<Individual> generatePopulation(PuzzleTheme puzzleTheme, int populationSize) {
		List<Individual> initialPopulation = new ArrayList<Individual>();
		
		for(int i = 0; i < populationSize; i++){
			Puzzle puzzle = new Puzzle(puzzleTheme);
			puzzle.shuffle();
			initialPopulation.add(new Individual(puzzle));
		}
		
		return initialPopulation;
	}
	
	private List<Individual> evolve(int mutationProbability, int size) throws BusinessException {
		List<Individual> newIndividuals = new ArrayList<Individual>();
		List<Individual> selectedIndividuals = new ArrayList<Individual>();
		
		List<Individual> offsprings;
		Individual offspring;
		int operator = 0;
		for (int i = 0; i < size; i++) {
			operator = RandomConstants.RANDOM.nextInt(100);
			if(operator < mutationProbability){
				selectedIndividuals = RandomSelector.select(this.population, 1);
				offspring = doMutation(selectedIndividuals.get(0));
				newIndividuals.add(offspring);
			} else {
				selectedIndividuals = RandomSelector.select(this.population, 2);
				offsprings = doCrossover(selectedIndividuals);
				newIndividuals.addAll(offsprings);
			}
			
		}
					
		return newIndividuals;
	}
	
	private List<Individual> doCrossover(List<Individual> individuals) throws BusinessException {
		//int operationNumber = RandomConstants.RANDOM.nextInt(10);
		CrossoverOperator operator = CrossoverOperatorFactory.getOperator(0);
		
		return operator.operate(individuals.get(0), individuals.get(1));
	}

	private Individual doMutation(Individual individual) throws BusinessException {
		int operationNumber = RandomConstants.RANDOM.nextInt(20);
		MutationOperator operator = MutationOperatorFactory.getOperator(operationNumber);
		
		return operator.operate(individual);
	}

	private void mergeInANewGeneration(List<Individual> newIndividuals) throws BusinessException {
		List<Individual> indList = new ArrayList<Individual>(this.population.getIndividuals());
		Collections.sort(indList, new IndividualComparator());
		List<Individual> newPopulationIndividuals = new ArrayList<Individual>();
		Individual newIndividual;
		
		for(int i = newIndividuals.size(); i < indList.size(); i++){
			newPopulationIndividuals.add(indList.get(i));
		}
		
		for (Individual individual : newIndividuals) {
			if(!exists(individual, newPopulationIndividuals)){
				newPopulationIndividuals.add(individual);
			} else {
				newIndividual = RegionRotateMutationOperator.getInstance().operate(individual);
				newPopulationIndividuals.add(newIndividual);
			}
		}
		
		this.population = new Population(newPopulationIndividuals);
	}
	
	private void mergeClimbed(List<Individual> selectedIndividuals, List<Individual> newIndividuals) {
		List<Individual> populationIndividuals = new ArrayList<Individual>(this.populationSize);
		
		int i = 0;
		while(populationIndividuals.size() < this.populationSize && i < newIndividuals.size()){
			populationIndividuals.add(newIndividuals.get(i));
			i++;
		}
		
		List<Individual> indList = new ArrayList<Individual>(this.population.getIndividuals());
		Collections.sort(indList, new IndividualComparator());
		i = 0;
		while(populationIndividuals.size() < this.populationSize){
			populationIndividuals.add(indList.get(i));
			i++;
		}
		
		this.population = new Population(populationIndividuals);
	}

	private boolean exists(Individual individual, List<Individual> newPopulationIndividuals) {
		boolean result = false;
		for (Individual individual2 : newPopulationIndividuals) {
			if(individual.equals(individual2)){
				result = true;
				break;
			}
		}
		return result;
	}
	
	private String buildLineToShow(int iteration, Individual populationBest, Individual best, long initialMilisecs) {
		float comparition = best.getPuzzle().compare(populationBest.getPuzzle());
		
		StringBuilder strb = new StringBuilder();
		//strb.append("Iteracion: ");
		strb.append(iteration);
		strb.append("\t");
		//strb.append("Mejor: ");
		strb.append(best.getFitnessValue());
		strb.append("\t");
		//strb.append("Maximo: ");	
		strb.append(best.getMaxPossibleFitness());
		strb.append("\t");
		//strb.append("Comparacion: ");
		strb.append(String.format("%.2f", comparition));
		strb.append("\t");
		//strb.append("Esperanza: ");
		strb.append(String.format("%.2f", this.population.getES()));
		strb.append("\t");
		//strb.append("Varianza: ");
		strb.append(String.format("%.2f", this.population.getVS()));
		strb.append("\t");
		//strb.append("Tiempo (ms): ");
		strb.append(System.currentTimeMillis() - initialMilisecs);
		return strb.toString();
	}

	public PuzzleTheme getPuzzleTheme() {
		return thePuzzleTheme;
	}
	
	public void setThePuzzleTheme(PuzzleTheme thePuzzleTheme) {
		this.thePuzzleTheme = thePuzzleTheme;
	}

	public int getPopulationSize() {
		return populationSize;
	}

	public void setPopulationSize(int populationSize) {
		this.populationSize = populationSize;
	}

	public int getIterationNumber() {
		return iterationNumber;
	}

	public void setIterationNumber(int iterationNumber) {
		this.iterationNumber = iterationNumber;
	}

	public boolean isShowLog() {
		return showLog;
	}

	public void setShowLog(boolean showLog) {
		this.showLog = showLog;
	}

}
